/*
 * GEHC Dicom Parser
 * Guangyao Lee
 * guangyaoli@ge.com
 * init: 2015/4/2
 */

 /*
  * DataReader
  * buffer: ArrayBuffer
  */
import {dictionary} from "hc-dicom-dict.es6";

export class Reader {
	constructor(buffer, littleEndian = true) {
        if (typeof buffer === "undefined") {
            throw new Error("No Buffer was specified!");
        }
		this.view = new DataView(buffer);
		this.littleEndian = littleEndian;
	}

	readInt8(byteOffset) {
		return this.view.getInt8(byteOffset);
	}

	readUint8(byteOffset) {
		return this.view.getUint8(byteOffset);
	}

	readInt16(byteOffset) {
		return this.view.getInt16(byteOffset, this.littleEndian);
	}

	readUint16(byteOffset) {
		return this.view.getUint16(byteOffset, this.littleEndian);
	}

	readInt32(byteOffset) {
		return this.view.getInt32(byteOffset, this.littleEndian);
	}

	readUint32(byteOffset) {
		return this.view.getUint32(byteOffset, this.littleEndian);
	}

	readFloat32(byteOffset) {
		return this.view.getFloat32(byteOffset, this.littleEndian);
	}
    
    readFloat64(byteOffset) {
		return this.view.getFloat64(byteOffset, this.littleEndian);
	}
    
	/*
	 * byteOffset: the offset to start reading from.
	 * size: size of the array
	 */
	readUint8Array(byteOffset, size) {
		let data = new Uint8Array(size),
			index = 0;

		for (let from = byteOffset,
				 to = byteOffset + size;
				 from < to;
				 from++) {

			data[index++] = this.readUint8(from);
		}
		return data;
	}

	/*
	 * byteOffset: the offset to start reading from.
	 * size: size of the array
	 */
	readUint16Array(byteOffset, size) {
		let data = new Uint16Array(size / 2),
			index = 0;

		for (let from = byteOffset,
				 to = byteOffset + size;
				 from < to;
				 from += 2) {

			data[index++] = this.readUint16(from);
		}
		return data;
	}

	/*
	 * read data as a hexadecimal string
	 * byteOffset: the offset to start reading from.
	 */
	readHex(byteOffset) {
		// read and convert to hex string
		let str = this.readUint16(byteOffset)
					  .toString(16)
					  .toUpperCase();

		const hexLength = 4; 
		// return padded value         
        return "0000".substr(0, hexLength - str.length) + str; 
	}

	/*
	 * read data as an number
	 * byteOffset: the offset to start reading from.
	 */
	readUint(byteOffset, bytes) {
        if (bytes === 1) {
			return this.readUint8(byteOffset);
		} else if (bytes === 2) {
			return this.readUint16(byteOffset);
		} else if (bytes === 4) {
			return this.readUint32(byteOffset);
		} else {
			console.warn(`Not a Uint: [byteOffset: ${byteOffset}, bytes: ${bytes}]`);
            return 0;
		}
	}
    
    readFloat(byteOffset, bytes) {
        if (bytes === 4) {
			return this.readFloat32(byteOffset);
		} else if (bytes === 8) {
            return this.readFloat64(byteOffset);
        } else {
			console.warn(`Not a Float: [byteOffset: ${byteOffset}, bytes: ${bytes}]`);
            return 0.0;
		}
    }
    
	/*
	 * byteOffset: the offset to start reading from.
	 * chars: the number of characters to read.
	 */
	readString(byteOffset, chars) {
		let result = "";
		for (let from = byteOffset,
				 to = byteOffset + chars;
				 from < to;
				 from++) {
			result += String.fromCharCode(this.readUint8(from));
		}
		return result;
	}
}

export class Parser {
	constructor() {
		// list of DICOM elements
		this.elements = {};

		// number of DICOM items
		this.numberOfItems = 0;

		this.pixelBuffer = [];
		this.littleEndian = true;
        
        // window width
        this.ww = -1;
        // window center
        this.wc = -1;
	}
    
    /* window width & window center */
    restoreWindowWidthAndWindowCenter() {
        this.ww = -1;
        this.wc = -1;
    }    
    set windowWidth(value) {
        this.ww = value;
    }
    get windowWidth() {
        if (this.ww === -1) {
            return Number.parseInt(this.elements["0028,1051"].value[0]);
        }
        return this.ww;
    }
    set windowCenter(value) {
        this.wc = value;
    }
    get windowCenter() {
        if (this.wc === -1) {
            return Number.parseInt(this.elements["0028,1050"].value[0]);
        }
        return this.wc;
    }
    
    get rescaleIntercept() {
        return Number.parseInt(this.elements["0028,1052"].value[0]);
    }
    get rescaleSlope() {
        return Number.parseInt(this.elements["0028,1053"].value[0]);
    }    
    /* annotations */
    // top left corner 
    get manfacturerModelName() {
        return this.elements["0008,1090"].value[0];
    }
    get stationName() {
        let e = this.elements["0008,1010"];
        if (e) {
            return e.value[0];
        }
        return "";
    }
    get exam() {
        return this.elements["0020,0010"].value[0];
    }
    get series() {
        return this.elements["0020,0011"].value[0];   
    }
    get imageNumber() {
        return this.elements["0020,0013"].value[0];
    }
    get dfov() {
        //throw new Error("not found");
    }
    get iterativeReconAnnotation() {
        let e = this.elements["0053,1040"];
        if (e) {
            return e.value[0];
        }
        return "";
    }
    // left
    
    // bottom left corner
    get kv() {
        return this.elements["0018,0060"].value[0];
    }
    get ma() {
        let e = this.elements["0018,0051"];
        if (e) {
            return e.value[0];
        }
        return "";
    }
    get noiseIndex() {
        //throw new Error("not found");
    }
    get largeBody() {/* type */
        //throw new Error("not found");
    }
    get sliceThickness() {
        return Number.parseFloat(this.elements["0018,0050"].value[0]).toFixed(3);
    }
    get tableSpeed() {
        let e = this.elements["0019,1023"];
        if (e) {
            return Number.parseFloat(e.value[0]).toFixed(2);
        }
        return "";
    }
    get scanPitchRatio() {
        let e = this.elements["0043,1027"];
        if (e) {
            return e.value[0];
        }
        return "";
    }
    get tilt() {
        return this.elements["0018,1120"].value[0];
    }
    get revolutionTime() {
        let e = this.elements["0018,9305"];
        if (e) {
            return e.value[0];
        }
        return "";
    }
    get acquisitionTime() {
        return this.elements["0008,0032"].value[0];
    }
    // top right corner
    get institutionName() {
        return this.elements["0008,0080"].value[0];
    }
    get patientID() {
        return this.elements["0010,0020"].value[0];
    }
    get patientName() {
        return this.elements["0010,0010"].value[0];
    }
    get acquisitionDate() {
        return this.elements["0008,0022"].value[0];
    }
    get rows() {
        return Number.parseInt(this.elements["0028,0010"].value[0]);
    }
    get cols() {
        return Number.parseInt(this.elements["0028,0011"].value[0]);
    }
    // right
    
    // bottom
    
    get annotations() {
        return {
            /* top left corner */
            systemName: `${this.manfacturerModelName} SYS#${this.stationName}`,
            exam: `Ex: ${this.exam}`,
            series: `Se: ${this.series}`,
            imageNumber: `Im: ${this.imageNumber}`,
            dfov: `DFOV ${this.dfov}`,
            iterativeReconAnnotation: `SOFT/+/${this.iterativeReconAnnotation}`,
            /* left */
            
            /* bottom left corner */
            kv: `KV ${this.kv}`,
            ma: `mA ${this.ma}`,
            noiseIndex: `${this.noiseIndex}~`,
            largeBody: this.largeBody,
            scanParameter: `${this.sliceThickness}mm/${this.tableSpeed} ${this.scanPitchRatio}`,
            tilt: `Tilt: ${this.tilt}`,
            time: `${this.revolutionTime}s /HE+ ${this.acquisitionTime}`,
            windowWidthAndWindowCenter: `W:${this.windowWidth} L:${this.windowCenter}`,
            
            /* top */ 
            
            /* top right corner */
            institutionName: this.institutionName,            
            patientID: this.patientID,
            patientName: this.patientName,
            date: this.acquisitionDate,
            rows: this.rows,            
            /* right */ 
            
            /* bottom */
            
        }
    }
    /* END: annotations */
    
    toString() {
        let es = this.elements,
            result = "";
        for (let e in this.elements) {
            let tag = es[e],
                v = tag.value;  // value is an array.
            
            result += `${e}: ${tag.vr}: ${tag.vl}:\t\t${tag.name}\t\t`;
            
            // wouldnot show if length great than 10
            let vlen = v.length;
            if (vlen < 10 && vlen > 1) {
                result += "[";
                let i = 0;
                for (let len = vlen; i < len - 1; i++) {
                    result += `${i}: ${v[i]}, `;
                }
                result += `${i}: ${v[i]}]`;
            } else if (vlen === 1) {
                result += `[${v[0]}]`;
            }
            result += "\n";
        }
        return result;
    }
    
    reset() {
        this.elements = {};
		this.numberOfItems = 0;
		this.pixelBuffer = [];
		this.littleEndian = true;
    }
	/*
	 * Append a DICOM element to the elements member object.
	 * Allows for easy retrieval of DICOM tag values from the tag name.
	 * If tags have same name (for the "unknown" and private tags cases),
     * a number is appended making the name unique. 
	 */
	appendDicomElement(element) {        
        let tag = element.tag;

		// increment the number of elements
		if (tag === "FFFE,E000") {
			this.numberOfItems++;
		}

		// make the name unique
		let count = 1;
		while (this.elements[tag]) {
			tag = `${element.tag}-${count++}`;
		}

		// store it
		this.elements[tag] = {
            vr   : element.vr,
            vl   : element.vl,
            value: element.value,
            name : element.name
        } 
	}

	/*
	 * reader: the raw data reader(DataReader)
	 * offset: the offset where to start to read
	 * @returns { an object containing the tags "group", "element" and "name"}
	 */
	readTag(reader, offset) {
		let group = reader.readHex(offset),
			element = reader.readHex(offset + 2),
            tag = group + "," + element,
			name = "hc::unknown";

		if (dictionary[tag]) {
			name = dictionary[tag][2];
		}

		return {group, element, name};
	}

	readElement(reader, offset, implicit) {
		const tagOffset = 4; // all tags' length is 4: group(2) + element(2)
        
		// tag: {group, element, name}
		let tag = this.readTag(reader, offset),
			vr,
			vl,
			vrOffset = 0,
			vlOffset = 0;
                
		// (private) Item group case
		if (tag.group === "FFFE") {
			vr = "N/A";
			vrOffset = 0;
			vl = reader.readUint32(offset + tagOffset + vrOffset);
			vlOffset = 4;
		}
		// non Item group case
		else {
			// implicit VR?
			if (implicit) {
				vr = "UN";  // 隐式VR时, VR的值需要在vrs中查找                
                let tag = tag.group + "," + tag.element;
				if (dictionary[tag]) {				
					vr = dictionary[tag][0];
				}
				
				vrOffset = 0; // 隐式 VR 时, VR 长度为 0	
				vl = reader.readUint32(offset + tagOffset + vrOffset);			
				vlOffset = 4; // 隐式 VR 时, vl 长度为 4
			}
			// explicit
			// 又分两种:
			//     1. 当 vr 为 "OB", "OW", "OF", "SQ", "UN" 之一时,
			//     			vr后会预留2字节长度, 且vlOffset 为 4
			//     2. 其它情况下: vr 后没有预留长度, 且vlOffset 为 2
			else {
				vrOffset = 2;
				vr = reader.readString(offset + tagOffset, vrOffset);

				// long representations
				if (["OB", "OW", "SQ", "OF", "UT", "UN"].indexOf(vr) > -1) {
					const reservedLength = 2;
					vl = reader.readUint32(offset + tagOffset + vrOffset + reservedLength);
					vlOffset = reservedLength + 4;
				}
				// short representation
				else {
					vl = reader.readUint16(offset + tagOffset + vrOffset);
					vlOffset = 2;
				}
			}
		}

		// check the value of VL
		if (vl === 0xFFFFFFFF) {
			vl = 0;
		}
        
		let data,
			dataOffset = offset + tagOffset + vrOffset + vlOffset;

        switch (vr) {
            case "US":
            case "UL":
                data = [reader.readUint(dataOffset, vl)];
                break;
            case "FL":
            case "FD":           
                data = [reader.readFloat(dataOffset, vl)];
                break;
            case "OX":
            case "OW":                    
                data = reader.readUint16Array(dataOffset, vl);
                break;
            case "OB":
            case "N/A":
                data = reader.readUint8Array(dataOffset, vl);
                break;
            default:
                data = reader.readString(dataOffset, vl).split("\\");
                break;
        }

		let elementOffset = tagOffset + vrOffset + vlOffset + vl;
		return {
			tag,
			vr,
			vl,
			data,
			offset: elementOffset
		}
	}

    validate(reader) {
        // [0, 128): reserved
        // [128, 132): "DICM"        
        const offset = 128, 
              magicWordLength = 4,        
		      magicWord = reader.readString(offset, magicWordLength);
        
		if (magicWord != "DICM") {
			throw new Error("Not a Valid DICOM File: no magic word[DICM] found");
		}
    }
    
	parse(buffer) {
        this.reset();
        
		// default readers
		let metaReader = new Reader(buffer),
			dataReader = new Reader(buffer);

		// step 1 ----------------------
        // check whether it is a valid dicom file
        this.validate(metaReader);        
		
		// step 2 ----------------------
		// 0x0002, 0x0000: MetaElementGroupLength
        let offset = 128 + 4,  // magic number: prefix(128) + "DICM"(4)
			implicit = false,
            element = this.readElement(metaReader, offset),
			metaLength = Number.parseInt(element.data, 10);

		offset += element.offset;

		// meta elements
		let metaStart = offset,
			metaEnd = metaStart + metaLength,
			i = metaStart;

		while (i < metaEnd) {
			// get the element
			element = this.readElement(metaReader, i, false); // meta element: always explicit VR 
			let syntax = element.data[0],
                tag = element.tag;

            /*
             * default: explicit VR - little endian
             * syntax === "1.2.840.10008.1.2.1"
             */
			// implicit VR - little endian
			if (syntax === "1.2.840.10008.1.2") {
				implicit = true;
			}       
            // explicit VR - big endian
			else if (syntax === "1.2.840.10008.1.2.2") {
				this.littleEndian = false;
				dataReader.littleEndian = false;
			}
			else if (syntax === "1.2.840.10008.1.2.1.99") {
				throw new Error(`Unsupported DICOM transfer syntax (Deflated Explicit VR): ${syntax}`);
			}
			else if (syntax.toString()
                        .match(/1.2.840.10008.1.2.4\.[0-9]{2}/) !== null) {
				throw new Error(`Unsupported DICOM transfer syntax (JPEG-*): ${syntax}`);
			}
            
			// store the element
			this.appendDicomElement({
				name   : tag.name,
				tag    : tag.group + "," + tag.element,
				vr     : element.vr,
				vl     : element.vl,
				value  : element.data
			});

			// increment offset
			i += element.offset;
		}

		let startedPixelItems = false,
			byteLength = buffer.byteLength;

		while (i < byteLength) {
			// get the element
			try {
				element = this.readElement(dataReader, i, implicit);
			} catch (ex) {
				console.warn(`Problem reading at ${i}/${byteLength}, after ${tag}\n-${ex}`);
			}
            
            let t = element.tag,
                tag = t.group + "," + t.element;
            
			// store pixel data from multiple items
			if (startedPixelItems) {
				if (tag === "FFFE,E000") {
					if (element.data.length === 4) {
						console.log("Skipping basic offset table");
					} else if (element.data.length !== 0) {
						console.log(`Concatenating multiple pixel data items, length: ${element.data.length}`);
                        
						let size = element.data.length + this.pixelBuffer.length,
							newBuffer = new Uint16Array(size);

						newBuffer.set(this.pixelBuffer, 0);
						newBuffer.set(element.data, this.pixelBuffer.length);
						this.pixelBuffer = newBuffer;
					}
				} else if (tag === "FFFE,E0DD") {
					startedPixelItems = false;
				} else {
					throw new Error(`Unexpected tag in encapsulated pixel data: ${element.tag.name}`);
				}
			}

			// check the pixel data tag
			if (tag === "7FE0,0010") {
				if (element.data.length !== 0) {
					this.pixelBuffer = element.data;
				} else {
					startedPixelItems = true;
				}
			}

			// store the element
			this.appendDicomElement({
				tag,
				name   : element.tag.name,
				vr     : element.vr,
				vl     : element.vl,
				value  : element.data
			});

			// increment offset
			i += element.offset;
		}
	}

	// chrome cannot support [new ImageData(rows, cols)] so far,
	// so we implement the interface by denoted a ImageData's instance.
	fillImageData(imgData) {
		let data = imgData.data,
			dataLength = data.length,
			ww = this.windowWidth,
			wc = this.windowCenter,
			rescaleIntercept = this.rescaleIntercept,
			rescaleSlope = this.rescaleSlope,
			rows = this.rows,
			cols = this.cols,
			max = wc + ww >> 1,
			min = max - ww,
			view = new DataView(this.pixelBuffer.buffer),
			offset = 0;

		for (let i = 0; i < dataLength; i += 4) {
			let value = view.getInt16(offset, this.littleEndian) *
                                rescaleSlope + rescaleIntercept;
            
			// increment offset
			offset += 2;

			data[i + 3] = 255; // alpha transparent
			if (value > max) {
				data[i] = data[i + 1] = data[i + 2] = 255;
				continue;
			} else if (value < min) {
				data[i] = data[i + 1] = data[i + 2] = 0;
				continue;
			}

			let grayValue = Number.parseInt((value - min) * 255 / ww, 10);
            data[i] = data[i + 1] = data[i + 2] = grayValue;
		}

		return imgData;
	}
}